package org.arl.modem.linktuner;

import jade.core.AID;
import jade.core.behaviours.SimpleBehaviour;
import jade.util.Logger;
import java.util.*;
import org.arl.modem.FAPIMessage;
import org.arl.modem.phy.Physical;


//continously spits out data
public class Exploit{
	
	protected SmartBoy smart_boy;
	protected Logger log;
	
	protected  AID notify;
	
	public static int ENABLE=0;
	private static int BEGIN_PKT_TRAIN_FLAG=0;
	private static boolean testPktTrainTxInProgress=false;
	
	private static int errorCnt=0;
	private static int testPktTxAttemptCnt=0;
	private static int testPktCnt=0;
	private static int testPktTrainCnt=0;
	private static int rxTestPktCnt=0;
	private static int rxTestPktCntPrev=0;
	private static int errorCntPrev=0;
	private static int testResultsAckCnt=0;

	private static int batchSize=4;
	private static int maxTestPktTrainCnt=20;

	private static int[] rxTestPktCntContainer=new int[maxTestPktTrainCnt];
	private static int[] errorCntContainer=new int[maxTestPktTrainCnt];

	public final static String NOTIFICATIONS = "exploit-ntf";
	
	public final static int EXPLOIT_PROTOCOL_ID=5;
	
	public final static int TYPE_QUERY_TEST_RESULTS_M=0xFF;
	public final static int TYPE_TEST_RESULT_QUERY_ACK_S=0xFE;
	
	public final static int TYPE_ENABLE_EXPLOITATION_SET_M=0xFD;
	public final static int TYPE_ENABLE_EXPLOITATION_SET_ACK_S=0xFC;
	
	private resendControlPacketBehaviour resendEnableExploitationSetRequest;
	private resendControlPacketBehaviour resendQueryTestResults;
	public int resendDelay=1000;
	public int resendMaxTries=4;

	
	protected stopWatch sw = new stopWatch();

	
	protected Exploit(SmartBoy smart_boy, Logger log){
		this.smart_boy=smart_boy;
		this.log=log;
		System.out.println("Exploit agent is loaded");
	}
	
	
	protected void handleMessage(FAPIMessage msg) {

		if(msg.getSender().equals(smart_boy.phy) && ENABLE==1 ){
			log.fine("recieved a phy layer notification "+ msg.get("type"));
			if(msg.get("type")=="RX.TESTPKT.CNT"){
				rxTestPktCnt++;
				int eCnt=msg.getAsInteger("errors", 0);
				log.fine("received a testpacket! error count = "+ eCnt);
				errorCnt=errorCnt+eCnt;
				log.fine("Current error count = " + errorCnt);
				log.fine(" rxTestPktCnt= " + rxTestPktCnt);
			}
			if(msg.get("type")=="TX.PKT.TIME"){
				sendTestPacketTrain(ParamSetter.ACTIVE_SCHEME, batchSize);
			}
		}
		
		switch (msg.getAsInteger("ExploitCommands",-1)){
			case TYPE_ENABLE_EXPLOITATION_SET_M:
				processEnableExploitation(msg,1);
				break;
			case TYPE_ENABLE_EXPLOITATION_SET_ACK_S:
//				smart_boy.removeBehaviour(resendEnableExploitationSetRequest);     
				log.fine("Master has received ack for request to set ENABLE parameter.");
				processEnableExploitation(msg,0);
				break;
			case TYPE_QUERY_TEST_RESULTS_M:
				processTestResultQuery(msg);
				break;
			case TYPE_TEST_RESULT_QUERY_ACK_S:
				processTestResultsAck(msg);
				break;
			case SmartBoyCommands.BEGIN_EX_COMMAND:
				sendTestPacketTrain(ParamSetter.ACTIVE_SCHEME, batchSize, true);
				break;
			case SmartBoyCommands.ENABLE_COMMAND:  
				enableExploitationSetRequest(1,true);
				break;
		}
	}
	private void sendTestPacketTrain(int activeScheme, int batchSize, boolean beginPktTrainFlag){
		if(beginPktTrainFlag){
			BEGIN_PKT_TRAIN_FLAG=1;
			testPktTrainTxInProgress=true;
			log.fine("set BEGIN_PKT_TRAIN_FLAG.");
			sw.start();
			sendTestPacketTrain(activeScheme, batchSize);
		}
		else{
			log.fine("failed to set BEGIN_PKT_TRAIN_FLAG.");
		}		
	}
	private void sendTestPacketTrain(int activeScheme, int batchSize){
		if(BEGIN_PKT_TRAIN_FLAG==1 && testPktTxAttemptCnt<batchSize){
			if(sendTestPacket(activeScheme)){
				testPktCnt++;
				log.fine("testPktCnt = "+testPktCnt +" , testPktTxAttemptCnt = "+(testPktTxAttemptCnt+1));
			}
			else{
				log.fine("failed to transmit test pkt");
			}
			testPktTxAttemptCnt++;
		}
		if(testPktTxAttemptCnt>=batchSize){
			sw.stop();
	        log.fine("elapsed time in milliseconds: " + sw.getElapsedTime());
			testPktTxAttemptCnt=0;
			BEGIN_PKT_TRAIN_FLAG=0;
			testPktTrainTxInProgress=false;
			testPktTrainCnt++;      //to indicate that we have finished transmitting a train of test packets.
			testSchemeParams();
			log.fine("done transmitting test pkt train.");
		}
	}
	//while trying to send out a test packet, do remember to add dummy/0-byte data and scheme, even though the docu-
	//mentation says there is no need for it. For some reference see PhysicalCommands.TX_ The TX_ method is not
	//called explicitly, rather it is invoked by reflection (ie dynamically when u type in the at commands).
	private boolean sendTestPacket(int activeScheme){
		log.fine("inside the sendTestPackets() method");
		FAPIMessage req=smart_boy.createMessage(FAPIMessage.REQUEST, smart_boy.phy);
		req.setData(new byte[0]);
		req.add("type", "TESTPKT");
		req.add("scheme", activeScheme);
	    req.add("lowpri", 0);
		FAPIMessage reply=smart_boy.request(req, 2000);
		if(reply != null && reply.getPerformative() == FAPIMessage.AGREE){
			log.fine("master has sent out a test packet");
			return true;
		}
		else {
			log.fine("not successful in sending out test packets");
			return false;
		}
	}

	//Gets called whenever the train of test packet transmissions finishes.
	private void testSchemeParams(){
		if(!testPktTrainTxInProgress && testPktTrainCnt<=maxTestPktTrainCnt){
			//do some condition checking here
			if(testPktTrainCnt>0){
				queryTestResults(testPktTrainCnt,true);		
				return;
			}
			//sendTestPacketTrain(ACTIVE_SCHEME,linkTunerCommands.batchSize, true);
		}
	}
		
	private boolean queryTestResults(int testPktTrainCnt, boolean invokeResendBehaviour){
		FAPIMessage req=smart_boy.createMessage(FAPIMessage.REQUEST,smart_boy.link);
		req.add("protocol",EXPLOIT_PROTOCOL_ID);
		//req.add("scheme",ACTIVE_SCHEME); 
		byte[] rQuery=new byte[2];
		rQuery[0]=(byte)TYPE_QUERY_TEST_RESULTS_M; //remember to try encoding the testPktTrainCnt somewhere in there....
		rQuery[1]=(byte)(testPktTrainCnt&0x000000FF);
		req.setData(rQuery);
		FAPIMessage reply=smart_boy.request(req, 2000);
		if(reply != null && reply.getPerformative() == FAPIMessage.AGREE){
			if(invokeResendBehaviour){
				resendQueryTestResults = new resendControlPacketBehaviour(resendDelay, resendMaxTries, System.currentTimeMillis(), TYPE_QUERY_TEST_RESULTS_M, testPktTrainCnt);
				smart_boy.addBehaviour(resendQueryTestResults);				
			}
			log.fine("master has queried for test results");
			return true;
		}
		else {
			log.warning("not successful in sending out test result query");
			diagnose(reply);
			return false;
		}
	}
	
	//returns an ack to the master with the errorCnt and the rxTestPktCnt
	//Packet format:
	//byte 0:TYPE_TEST_RESULT_QUERY_ACK_S, byte1:identifier data, bytes2-5: rxTestPktCnt, bytes6-9:errorCnt.
	private void processTestResultQuery(FAPIMessage msg){
		log.fine("inside the processTestResultQuery method");
		FAPIMessage req=smart_boy.createMessage(FAPIMessage.REQUEST,smart_boy.link);
		req.add("protocol",EXPLOIT_PROTOCOL_ID);
		byte[] rRsp=new byte[10];
		rRsp[0]=(byte)TYPE_TEST_RESULT_QUERY_ACK_S;
		rRsp[1]=msg.getData()[1]; //contains an identifier to ensure that the ack corresponds to the correct query
		byte[] v1 = intToByteArray(rxTestPktCnt);
		byte[] v2 = intToByteArray(errorCnt);
		System.arraycopy(v1,0,rRsp,2,4);
		System.arraycopy(v2,0,rRsp,6,4);
		req.setData(rRsp);
		FAPIMessage reply=smart_boy.request(req, 2000);
		if(reply != null && reply.getPerformative() == FAPIMessage.AGREE){
			log.fine("slave has processed the test result query and sent back an ack");
			//return true;
		}
		else {
			log.warning("slave is not successful in processing test result query");
			diagnose(reply);
			//return false;
		}
	}
	
	private void processTestResultsAck(FAPIMessage msg){
		log.fine("inside the processTestResultsAck method");
		//check if ack and query match
		if(((byte)(testPktTrainCnt&0x000000FF))==msg.getData()[1]){
			log.fine("test result ack matches the latest query");
//			smart_boy.removeBehaviour(resendQueryTestResults);
			byte[] v1=new byte[4];
			byte[] v2=new byte[4];
			System.arraycopy(msg.getData(), 2, v1, 0, 4);
			System.arraycopy(msg.getData(), 6, v2, 0, 4);
			int t1=byteArrayToInt(v1);
			int t2=byteArrayToInt(v2);	
			//remember that testPktTrainCnt starts from 1, so u need to subtract 1 from it so that the container index starts from zero.
			if(testPktTrainCnt<rxTestPktCntContainer.length && testPktTrainCnt>0){
				if(testPktTrainCnt==1){
					rxTestPktCntContainer[testPktTrainCnt-1]=t1;
					errorCntContainer[testPktTrainCnt-1]=t2;
				}
				else{
					rxTestPktCntContainer[testPktTrainCnt-1]=t1-rxTestPktCntPrev;
					errorCntContainer[testPktTrainCnt-1]=t2-errorCntPrev;
				}
				rxTestPktCntPrev=t1;
				errorCntPrev=t2;
				testResultsAckCnt++;
				log.fine("rxTestPktCnt = "+ t1 + " errorCnt = " + t2);
				log.fine("rxTestPktCntContainer["+(testPktTrainCnt-1)+"]"+"="+rxTestPktCntContainer[testPktTrainCnt-1]+"  "+"errorCntContainer["+(testPktTrainCnt-1)+"]"+"="+errorCntContainer[testPktTrainCnt-1]);
				/*
				 *log.fine("currentSetSchemeParam = "+currentSetSchemeParam[0]+" "+currentSetSchemeParam[1]+" "+currentSetSchemeParam[2]+" "+currentSetSchemeParam[3]);
				//Note that for BER calculation, the packet size is hard-coded to be 9.
				log.fine("Nc= "+Scheme_Params_Values[0][currentSetSchemeParam[0]]+", Np= "+Scheme_Params_Values[1][currentSetSchemeParam[1]]+ ", BER= "+(errorCntContainer[testPktTrainCnt-1]/(rxTestPktCntContainer[testPktTrainCnt-1])*9*8));
				log.fine("Nc, Np, rxTestPktCntContainer, errorCntContainer = "+Scheme_Params_Values[0][currentSetSchemeParam[0]]+" "+Scheme_Params_Values[1][currentSetSchemeParam[1]]+" "
				                                                               +rxTestPktCntContainer[testPktTrainCnt-1]+" "+errorCntContainer[testPktTrainCnt-1]);
 
				 */
			}
			else{
				log.warning("container index out of bounds");
				return;
			}
		}
	}
	
	private void enableExploitationSetRequest(int enable, boolean invokeResendBehaviour){
		FAPIMessage req=smart_boy.createMessage(FAPIMessage.REQUEST,smart_boy.link);
		req.add("protocol",EXPLOIT_PROTOCOL_ID);
		byte[] rScheme=new byte[2];
		rScheme[0]=(byte)TYPE_ENABLE_EXPLOITATION_SET_M;
		rScheme[1]=(byte)enable;
		req.setData(rScheme);
		FAPIMessage reply=smart_boy.request(req, 2000);
		if(reply != null && reply.getPerformative() == FAPIMessage.AGREE){
			if(invokeResendBehaviour){
//				resendEnableExploitationSetRequest = new resendControlPacketBehaviour(resendDelay, resendMaxTries, System.currentTimeMillis(), TYPE_ENABLE_EXPLOITATION_SET_M, enable);
//				smart_boy.addBehaviour(resendEnableExploitationSetRequest);				
			}
			log.fine("master has sent a request to enable exploitation");
		}
		else {
			log.warning("not successful in sending out enable exploitation request");
			diagnose(reply);
		}
	}
	
	//call with ack ==1 if you want to send ack. use with ack==0 if u just want to set your own enable.
	private void processEnableExploitation(FAPIMessage msg, int ack){
		byte enable=msg.getData()[1];
		if(enable==0){
			ENABLE=0;
			log.fine("Set ENABLE=0");
		}else if(enable==1){
			ENABLE=1;
			log.fine("Set ENABLE=1");
		}else {
			log.warning("Received invalid value of ENABLE");
		}
		if(ack==1){
			enableExploitationSetRequestAck(msg);
		}
	}
	
	private void enableExploitationSetRequestAck(FAPIMessage msg){
		FAPIMessage req=smart_boy.createMessage(FAPIMessage.REQUEST,smart_boy.link);
		req.add("protocol",EXPLOIT_PROTOCOL_ID);
		byte[] rScheme=new byte[2];
		rScheme[0]=(byte)TYPE_ENABLE_EXPLOITATION_SET_ACK_S;
		byte enable=msg.getData()[1];
		rScheme[1]=enable;     
		req.setData(rScheme);
		FAPIMessage reply=smart_boy.request(req, 2000);
		if(reply != null && reply.getPerformative() == FAPIMessage.AGREE){
			log.fine("acknowledged request to set ENABLE param");
		}
		else {
			log.warning("not successful in sending out acknowldgement to request to set ENABLE param");
			diagnose(reply);
		}
		
	}
	

	private class resendControlPacketBehaviour extends SimpleBehaviour{
		private int _delay;
		private int _ctrlPktType;
		private int _maxTries;
		private long _timeInvocation;
		private int state;
		private int _ctrlPktData;
		resendControlPacketBehaviour(int delay, int maxTries, long timeInvocation, int ctrlPktType, int ctrlPktData){
			log.fine("resendControlPacketBehaviour constructor invoked!");
			this._delay=delay;
			this._ctrlPktType=ctrlPktType;
			this._maxTries=maxTries;
			this._timeInvocation=timeInvocation;
			this.state=0;
			this._ctrlPktData=ctrlPktData;
		}
		public void action() {
			long t=System.currentTimeMillis()-_timeInvocation;
			if(t>=_delay*(state+1)){
				if(state==0){
					log.fine("state = 0, about to call block(), time diff = "+t);
					block(_delay);
				}else if(state>_maxTries){
					log.warning("Slave modem doesn't seem to be responding");
				}else if(state>0 && state<(_maxTries+1)){     
					log.fine("state = "+state+", about to enter switch() block");
					switch(_ctrlPktType){
						case TYPE_ENABLE_EXPLOITATION_SET_M:
							log.fine("Timer has been triggered. calling enableExploitationSetRequest(), time diff = "+t);
							enableExploitationSetRequest(_ctrlPktData,false);
							break;
						case TYPE_QUERY_TEST_RESULTS_M:
							log.fine("Timer has been triggered. calling queryTestResults(), time diff = "+t);
							queryTestResults(_ctrlPktData,false);
							break;
					}
				}
				state++;
			}
		}
		public boolean done() {
			return (state>_maxTries);
		}
	}

	
	//helper functions	
	public static final byte[] intToByteArray(int value) {
        return new byte[] {
                (byte)(value >>> 24),
                (byte)(value >>> 16),
                (byte)(value >>> 8),
                (byte)value};
	}
	public static final int byteArrayToInt(byte [] b) {
        return (b[0] << 24)
                + ((b[1] & 0xFF) << 16)
                + ((b[2] & 0xFF) << 8)
                + (b[3] & 0xFF);
	}

	private void diagnose(FAPIMessage reply){
		if(reply==null)log.fine("reply==null");
		if(reply.getPerformative()==FAPIMessage.FAILURE)log.fine("perf=failiure");
		if(reply.getPerformative()==FAPIMessage.NOT_UNDERSTOOD)log.fine("perf=not-understood");
		if(reply.getPerformative()==FAPIMessage.REFUSE)log.fine("perf=refuse");
		if(reply.getPerformative()==FAPIMessage.DISCONFIRM)log.fine("perf=disconfirm");
		log.fine("exiting diagnose");
	}

}
